# Задача Task 
# В предыдущем уроке мы посмотрели как создавать корутины и запускать их. При этом конкурентного 
# выполнения корутин не было, а ведь суть асинхронного программирования именно в этом. Для 
# реализации кооперативной многозадачности, в которой управление передается от одной корутины к 
# другой, нужно использовать задачи (объекты класса Task).


# Создание и запуск задачи. Метод asyncio.create_task().
# Задачи представляют собой обертки для корутин и используются для их конкурентного выполнения. Для создания задачи 
# используется функция asyncio.create_task().
# Возьмем пример из предыдущего урока и попробуем выполнить корутину example_coroutine(), обернув ее в задачу. 

# import asyncio


# async def example_coroutine():
#     await asyncio.sleep(1) 
#     print("Hello from coroutine!")

# async def main():
#     task = asyncio.create_task(example_coroutine())  # создаем задачу из корутины example_coroutine()
#     await task # ждем выполнения задачи

# asyncio.run(main())

# Функция asyncio.create_task() принимает объект корутины и возвращает объект класса Task. Возвращаемая задача,
# сразу добавляется в цикл событий и планируется в выполнению. Выполняться же она начнет при ближайшем
# переключении контекста (обычно это ключевое слово await).

# Если мы посмотрим type() задачи, мы увидим там следующее: <class '_asyncio.Task'>.

# print(type(asyncio.create_task(example_coroutine())))

# >>> <class '_asyncio.Task'>
# Объекты задачи, как и корутины, всегда следует запускать с оператором await.
######################################

# Выполнение нескольких задач конкурентно. Метод asyncio.gather().
# Как только задача создана из корутины, она становится независимой. Это означает, что выполнение задачи больше не 
# связано напрямую с корутиной, которая её породила. Задача будет запланирована и управляема циклом событий , который определяет,
# когда задача получит возможность, чтобы начать свое выполнение. Цикл событий предоставляет такую возможность всем задачам,
# запланированным к запуску, при первой же смене контекста (await), и запускает её (задачу) асинхронно.

# Попробуем создать в нашем примере 10 задач и запустить их конкурентно.  Для создания задач используем метод asyncio.create_task(),
# а для конкурентного запуска метод asyncio.gather().

# import asyncio
# import time

# async def example_coroutine():
#     await asyncio.sleep(1)
#     print("Hello from coroutine!")

# async def main():
#     tasks = []
#     for _ in range(10):
#         task = asyncio.create_task(example_coroutine())  # создаем 10 задач
#         tasks.append(task)  # добавляем все задачи в список tasks
#     await asyncio.gather(*tasks)  # запускаем все задачи из списка tasks

# start = time.perf_counter()

# asyncio.run(main())

# print(time.perf_counter() - start)



#############
# Аналогия из реальной жизни с asyncio.create_task() и asyncio.gather():
# Вы менеджер в ресторане, и у вас есть несколько поваров, работающих одновременно. У каждого из поваров есть 
# своя специализация и набор задач, которые нужно выполнить. Ваша задача как менеджера - назначать задачи поварам и
# официантам и следить за их выполнением. А также в случае необходимости следить, чтобы некоторые заказы готовились
# и подавались одновременно.   

# В данном случае менеджер аналогичен циклу событий, а повара и официанты — асинхронным задачам (task). 
# Когда вы назначаете задачу повару с помощью asyncio.create_task(), вы позволяете этому повару начать
# выполнение задачи, не ожидая завершения его работы, и переходите к назначению других задач другим поварам.

# Как только повар заканчивает свою задачу, он сообщает об этом вам (задача завершается). Вы, в свою очередь,
# можете сказать официанту (другой корутине) забрать блюдо и передать его клиенту. 

# Таким образом, asyncio.create_task() позволяет вам, как менеджеру, работать с несколькими поварами/официантами
# конкурентно, управлять их задачами и обрабатывать результаты по мере их выполнения. А asyncio.gather() позволяет
# готовить заказы одновременно.
# import asyncio

# # Список поваров.
# chef_list = ['', 'Франсуа', 'Жан', 'Марсель']


# async def cook_order(order_number, dish):
#     # Повар готовит блюдо
#     print(f"Повар {chef_list[order_number]} начинает готовить заказ №{order_number}: {dish}")
#     await asyncio.sleep(2)  # Имитация времени на готовку
#     print(f"Заказ №{order_number}: {dish} готов!")
#     return f"{dish} для заказа №{order_number}"


# async def serve_order(order_number, dish):
#     # Официант подает блюдо
#     await cook_order(order_number, dish)
#     print(f"Официант подает {dish}")


# async def manager():
#     # Менеджер (событийный цикл) назначает задачи
#     orders = [(1, 'Салат'), (2, 'Стейк'), (3, 'Суп')]
#     tasks = [asyncio.create_task(serve_order(order_number, dish)) for order_number, dish in orders]

#     # Ожидаем завершения всех задач
#     await asyncio.gather(*tasks)


# # Запуск событийного цикла
# asyncio.run(manager())
##########################################################

# Важно! Сохраняйте ссылку на результат функции asyncio.create_task(), чтобы задача не исчезла в процессе выполнения.
# Цикл событий сохраняет только слабые ссылки на задачи (тип ссылки в Python, которая не препятствует удалению объекта,
#                                                         на который она ссылается). Задача, на которую больше нигде
# нет ссылок, кроме слабых ссылок цикла событий, может быть удалена сборщиком мусора в любое время, даже до того,
# как она будет выполнена. Для надежных фоновых задач типа "запустил и забыл" нужно собрать их в коллекцию
# (множество, список или кортеж), как мы это делали в примерах выше.

# Ниже продемонстрирован плохой пример — создание задачи без сохранения ссылок, т. е. без создания переменных или списка задач.
# Код ниже хоть и запустится, но в других ситуациях задача может быть удалена сборщиком мусора.
import asyncio

async def my_task():
    print(f"{'-' * 10}\nRunning my task")
    await asyncio.sleep(1)
    print(f"Task complete\n{'-' * 10}")

async def main():
    # Создаем задачу без сохранения ссылки на нее
    asyncio.create_task(my_task())
    # Здесь произойдет запуск задачи, однако стоит помнить,
    # что эта задача может быть удалена сборщиком мусора в любой момент.
    await asyncio.sleep(2)

asyncio.run(main())